// xmenu.c
#include <X11/Xlib.h>
#include <X11/Xft/Xft.h>
#include <X11/keysym.h>
#include <X11/Xatom.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <sys/wait.h>
#include <unistd.h>
#include <stdbool.h>
#include <math.h>
#include <time.h>

#define WIDTH 800
#define HEIGHT 400
#define PADDING 15
#define MAX_ITEMS 4000
#define MAX_INPUT_LEN 256
#define MAX_VISIBLE 8
#define FONT_SIZE 28
#define HINT_FONT_SIZE 20
#define FONT_NAME "DejaVu Sans Mono"
#define TARGET_FPS 60
#define FRAME_TIME_US (1000000 / TARGET_FPS)

#define POPUP_POSITION_TOP 1
#define POPUP_POSITION_BOTTOM 0
#define DEFAULT_POPUP_POSITION POPUP_POSITION_TOP

#define COLOR_BG 0x000000
#define COLOR_INPUT_BG 0x282828
#define COLOR_INPUT_BG_ALPHA 0.9
#define COLOR_SEPARATOR 0x505050
#define COLOR_TEXT 0xFFFFFF
#define COLOR_HINT 0x8A8A8A
#define COLOR_SELECTION 0x006699

typedef struct
{
    char text[512];
    char basename[256];
    bool is_match;
} MenuItem;

MenuItem items[MAX_ITEMS];
int item_count = 0;
char input_str[MAX_INPUT_LEN] = "";
size_t cursor = 0;
int selected_idx = 0;
int scroll_offset = 0;
bool running = true;
bool redraw_needed = true;
bool command_executed = false;
static pid_t child_pid = -1;

Display *dpy;
Window win;
GC gc;
Visual *vis;
Colormap cmap;
Pixmap backbuffer;
XftFont *main_font;
XftFont *hint_font;
XftColor color_bg;
XftColor color_input_bg;
XftColor color_separator;
XftColor color_text;
XftColor color_hint;
XftColor color_selection;
int screen;
int screen_width, screen_height;
int window_x, window_y;

const char *get_basename(const char *path)
{
    const char *slash = strrchr(path, '/');
    return (slash) ? (slash + 1) : path;
}

bool init_colors()
{
    if (!XftColorAllocName(dpy, vis, cmap, "#000000", &color_bg))
        return false;
    if (!XftColorAllocName(dpy, vis, cmap, "#282828", &color_input_bg))
        return false;
    if (!XftColorAllocName(dpy, vis, cmap, "#505050", &color_separator))
        return false;
    if (!XftColorAllocName(dpy, vis, cmap, "#FFFFFF", &color_text))
        return false;
    if (!XftColorAllocName(dpy, vis, cmap, "#00ff00", &color_hint))
        return false;
    if (!XftColorAllocName(dpy, vis, cmap, "#006699", &color_selection))
        return false;
    return true;
}

bool init_fonts()
{
    char main_font_str[256];
    snprintf(main_font_str, sizeof(main_font_str), "%s:pixelsize=%d", FONT_NAME, FONT_SIZE);
    main_font = XftFontOpenName(dpy, screen, main_font_str);
    if (!main_font)
    {
        fprintf(stderr, "无法加载主字体: %s\n", main_font_str);
        return false;
    }

    char hint_font_str[256];
    snprintf(hint_font_str, sizeof(hint_font_str), "%s:pixelsize=%d", FONT_NAME, HINT_FONT_SIZE);
    hint_font = XftFontOpenName(dpy, screen, hint_font_str);
    if (!hint_font)
    {
        fprintf(stderr, "无法加载提示字体: %s\n", hint_font_str);
        XftFontClose(dpy, main_font);
        return false;
    }
    return true;
}

bool init_x11()
{
    dpy = XOpenDisplay(NULL);
    if (!dpy)
    {
        fprintf(stderr, "无法打开X显示\n");
        return false;
    }

    screen = DefaultScreen(dpy);
    vis = DefaultVisual(dpy, screen);
    cmap = DefaultColormap(dpy, screen);
    screen_width = DisplayWidth(dpy, screen);
    screen_height = DisplayHeight(dpy, screen);

    window_x = (screen_width - WIDTH) / 2;
#if DEFAULT_POPUP_POSITION == POPUP_POSITION_TOP
    window_y = 0;
#else
    window_y = screen_height - HEIGHT;
#endif

    XSetWindowAttributes attr;
    attr.override_redirect = True;
    attr.background_pixel = BlackPixel(dpy, screen);
    attr.event_mask = ExposureMask | KeyPressMask | ButtonPressMask |
                      ButtonReleaseMask | PointerMotionMask | ButtonMotionMask;

    win = XCreateWindow(
        dpy, DefaultRootWindow(dpy),
        window_x, window_y, WIDTH, HEIGHT,
        0, DefaultDepth(dpy, screen),
        InputOutput, vis,
        CWOverrideRedirect | CWBackPixel | CWEventMask, &attr);

    backbuffer = XCreatePixmap(
        dpy, win, WIDTH, HEIGHT,
        DefaultDepth(dpy, screen));

    gc = XCreateGC(dpy, backbuffer, 0, NULL);
    if (!gc)
    {
        fprintf(stderr, "无法创建GC\n");
        XCloseDisplay(dpy);
        return false;
    }

    Atom net_wm_state = XInternAtom(dpy, "_NET_WM_STATE", False);
    Atom net_wm_state_above = XInternAtom(dpy, "_NET_WM_STATE_ABOVE", False);
    XChangeProperty(
        dpy, win, net_wm_state, XA_ATOM, 32,
        PropModeReplace, (unsigned char *)&net_wm_state_above, 1);

    if (!init_colors() || !init_fonts())
    {
        if (gc)
            XFreeGC(dpy, gc);
        if (backbuffer)
            XFreePixmap(dpy, backbuffer);
        if (win)
            XDestroyWindow(dpy, win);
        XCloseDisplay(dpy);
        return false;
    }

    XMapWindow(dpy, win);
    XSetInputFocus(dpy, win, RevertToParent, CurrentTime);
    XFlush(dpy);
    return true;
}

int get_main_text_width(const char *str)
{
    if (!str || !*str)
        return 0;
    XGlyphInfo info;
    XftTextExtentsUtf8(dpy, main_font, (const FcChar8 *)str, strlen(str), &info);
    return info.width;
}

int get_hint_text_width(const char *str)
{
    if (!str || !*str)
        return 0;
    XGlyphInfo info;
    XftTextExtentsUtf8(dpy, hint_font, (const FcChar8 *)str, strlen(str), &info);
    return info.width;
}

void draw_rect(int x, int y, int width, int height, XftColor *color, float alpha)
{
    if (alpha >= 1.0f)
    {
        XSetForeground(dpy, gc, color->pixel);
        XFillRectangle(dpy, backbuffer, gc, x, y, width, height);
        return;
    }

    XftDraw *draw = XftDrawCreate(dpy, backbuffer, vis, cmap);
    XftColor transp_color = *color;
    transp_color.color.alpha = (unsigned short)(alpha * 65535);

    XftDrawRect(draw, &transp_color, x, y, width, height);
    XftDrawDestroy(draw);
}

void draw_main_text(int x, int y, const char *str, XftColor *color)
{
    if (!str || !*str)
        return;
    y += main_font->ascent;
    XftDraw *draw = XftDrawCreate(dpy, backbuffer, vis, cmap);
    XftDrawStringUtf8(draw, color, main_font, x, y, (const FcChar8 *)str, strlen(str));
    XftDrawDestroy(draw);
}

void draw_hint_text(int x, int y, const char *str, XftColor *color)
{
    if (!str || !*str)
        return;
    y += hint_font->ascent;
    XftDraw *draw = XftDrawCreate(dpy, backbuffer, vis, cmap);
    XftDrawStringUtf8(draw, color, hint_font, x, y, (const FcChar8 *)str, strlen(str));
    XftDrawDestroy(draw);
}

void draw_cursor(int x, int y)
{
    y += main_font->ascent;
    draw_rect(x, y - main_font->height, 2, main_font->height, &color_text, 1.0f);
}

void read_items_from_stdin()
{
    char buf[512];
    item_count = 0;
    while (fgets(buf, sizeof(buf), stdin) && item_count < MAX_ITEMS)
    {
        size_t len = strlen(buf);
        while (len > 0 && isspace((unsigned char)buf[len - 1]))
            buf[--len] = '\0';

        strncpy(items[item_count].text, buf, sizeof(items[item_count].text) - 1);
        items[item_count].text[sizeof(items[item_count].text) - 1] = '\0';

        const char *basename = get_basename(buf);
        strncpy(items[item_count].basename, basename, sizeof(items[item_count].basename) - 1);
        items[item_count].basename[sizeof(items[item_count].basename) - 1] = '\0';

        items[item_count].is_match = true;
        item_count++;
    }
    redraw_needed = true;
}

void filter_items()
{
    int input_len = strlen(input_str);
    if (input_len == 0)
    {
        for (int i = 0; i < item_count; i++)
            items[i].is_match = true;
        selected_idx = 0;
        scroll_offset = 0;
        redraw_needed = true;
        return;
    }

    int match_count = 0;
    int first_match_idx = -1;
    for (int i = 0; i < item_count; i++)
    {
        const char *basename = items[i].basename;
        int basename_len = strlen(basename);

        bool match = (basename_len >= input_len);
        if (match)
        {
            for (int j = 0; j < input_len; j++)
            {
                if (tolower((unsigned char)input_str[j]) != tolower((unsigned char)basename[j]))
                {
                    match = false;
                    break;
                }
            }
        }

        items[i].is_match = match;
        if (match)
        {
            match_count++;
            if (first_match_idx == -1)
                first_match_idx = i;
        }
    }

    selected_idx = (match_count == 0) ? -1 : first_match_idx;
    int center_pos = MAX_VISIBLE / 2;
    scroll_offset = selected_idx - center_pos;
    if (scroll_offset < 0)
        scroll_offset = 0;

    redraw_needed = true;
}

void execute_command(const char *cmd)
{
    if (!cmd || strlen(cmd) == 0)
        return;

    if (child_pid != -1 || command_executed)
        return;

    pid_t pid = fork();
    if (pid == 0)
    {
        char *args[] = {(char *)cmd, NULL};
        freopen("/dev/null", "r", stdin);
        freopen("/dev/null", "w", stdout);
        freopen("/dev/null", "w", stderr);
        execvp(args[0], args);
        exit(EXIT_FAILURE);
    }
    else if (pid > 0)
    {
        child_pid = pid;
        command_executed = true;
        running = false;
    }
}

void check_child_process()
{
    if (child_pid == -1)
        return;

    int status;
    pid_t result = waitpid(child_pid, &status, WNOHANG);
    if (result == child_pid || result == -1)
    {
        child_pid = -1;
    }
}

int get_cursor_x_pos(int start_x)
{
    if (cursor == 0)
        return start_x;

    char temp[MAX_INPUT_LEN];
    strncpy(temp, input_str, cursor);
    temp[cursor] = '\0';
    return start_x + get_main_text_width(temp);
}

void handle_key_press(XKeyEvent *ev)
{
    if (command_executed)
        return;

    KeySym keysym = XKeycodeToKeysym(dpy, ev->keycode, 0);
    char buf[32];
    int len = XLookupString(ev, buf, sizeof(buf), &keysym, NULL);
    bool ctrl_pressed = (ev->state & ControlMask) != 0;

    if (ctrl_pressed)
    {
        switch (keysym)
        {
        case XK_h:
            if (cursor > 0)
            {
                cursor--;
                memmove(&input_str[cursor], &input_str[cursor + 1], MAX_INPUT_LEN - cursor - 1);
                filter_items();
            }
            return;

        case XK_d:
            if (cursor < strlen(input_str))
            {
                memmove(&input_str[cursor], &input_str[cursor + 1], MAX_INPUT_LEN - cursor - 1);
                filter_items();
            }
            return;

        case XK_k:
            input_str[cursor] = '\0';
            filter_items();
            return;

        case XK_u:
            input_str[0] = '\0';
            cursor = 0;
            filter_items();
            return;

        case XK_n:
            if (selected_idx != -1)
            {
                int next_idx = selected_idx + 1;
                while (next_idx < item_count && !items[next_idx].is_match)
                    next_idx++;
                if (next_idx < item_count)
                {
                    selected_idx = next_idx;
                    scroll_offset = selected_idx - (MAX_VISIBLE / 2);
                    if (scroll_offset < 0)
                        scroll_offset = 0;
                    redraw_needed = true;
                }
            }
            return;

        case XK_p:
            if (selected_idx != -1)
            {
                int prev_idx = selected_idx - 1;
                while (prev_idx >= 0 && !items[prev_idx].is_match)
                    prev_idx--;
                if (prev_idx >= 0)
                {
                    selected_idx = prev_idx;
                    scroll_offset = selected_idx - (MAX_VISIBLE / 2);
                    if (scroll_offset < 0)
                        scroll_offset = 0;
                    redraw_needed = true;
                }
            }
            return;

        case XK_f:
            if (cursor < strlen(input_str))
            {
                cursor++;
                redraw_needed = true;
            }
            return;

        case XK_b:
            if (cursor > 0)
            {
                cursor--;
                redraw_needed = true;
            }
            return;

        case XK_a:
            cursor = 0;
            redraw_needed = true;
            return;

        case XK_e:
            cursor = strlen(input_str);
            redraw_needed = true;
            return;
        }
    }

    switch (keysym)
    {
    case XK_Escape:
        running = false;
        break;

    case XK_Return:
    case XK_KP_Enter:
    {
        const char *cmd = NULL;
        if (selected_idx != -1 && items[selected_idx].is_match)
            cmd = items[selected_idx].text;
        else if (strlen(input_str) > 0)
            cmd = input_str;
        if (cmd)
            execute_command(cmd);
        break;
    }

    case XK_BackSpace:
        if (cursor > 0)
        {
            cursor--;
            memmove(&input_str[cursor], &input_str[cursor + 1], MAX_INPUT_LEN - cursor - 1);
            filter_items();
        }
        break;

    case XK_Delete:
        if (cursor < strlen(input_str))
        {
            memmove(&input_str[cursor], &input_str[cursor + 1], MAX_INPUT_LEN - cursor - 1);
            filter_items();
        }
        break;

    case XK_Down:
        if (selected_idx != -1)
        {
            int next_idx = selected_idx + 1;
            while (next_idx < item_count && !items[next_idx].is_match)
                next_idx++;
            if (next_idx < item_count)
            {
                selected_idx = next_idx;
                scroll_offset = selected_idx - (MAX_VISIBLE / 2);
                if (scroll_offset < 0)
                    scroll_offset = 0;
                redraw_needed = true;
            }
        }
        break;

    case XK_Up:
        if (selected_idx != -1)
        {
            int prev_idx = selected_idx - 1;
            while (prev_idx >= 0 && !items[prev_idx].is_match)
                prev_idx--;
            if (prev_idx >= 0)
            {
                selected_idx = prev_idx;
                scroll_offset = selected_idx - (MAX_VISIBLE / 2);
                if (scroll_offset < 0)
                    scroll_offset = 0;
                redraw_needed = true;
            }
        }
        break;

    case XK_Right:
        if (cursor < strlen(input_str))
        {
            cursor++;
            redraw_needed = true;
        }
        break;

    case XK_Left:
        if (cursor > 0)
        {
            cursor--;
            redraw_needed = true;
        }
        break;

    case XK_Home:
        cursor = 0;
        redraw_needed = true;
        break;

    case XK_End:
        cursor = strlen(input_str);
        redraw_needed = true;
        break;

    default:
        if (len > 0 && buf[0] >= 32 && buf[0] <= 126 && strlen(input_str) < MAX_INPUT_LEN - 1)
        {
            memmove(&input_str[cursor + 1], &input_str[cursor], MAX_INPUT_LEN - cursor - 1);
            input_str[cursor++] = buf[0];
            filter_items();
        }
        break;
    }
}

void handle_mouse_event(XButtonEvent *ev)
{
    if (command_executed)
        return;

    if (ev->window != win)
    {
        running = false;
        return;
    }

    // Mouse scroll handling (Button4 = scroll up, Button5 = scroll down)
    if (ev->type == ButtonPress && (ev->button == 4 || ev->button == 5))
    {
        if (selected_idx == -1)
            return;

        int target_idx = selected_idx;
        if (ev->button == 4)
        {
            target_idx = selected_idx - 1;
            while (target_idx >= 0 && !items[target_idx].is_match)
                target_idx--;
        }
        else if (ev->button == 5)
        {
            target_idx = selected_idx + 1;
            while (target_idx < item_count && !items[target_idx].is_match)
                target_idx++;
        }

        if (target_idx >= 0 && target_idx < item_count)
        {
            selected_idx = target_idx;
            scroll_offset = selected_idx - (MAX_VISIBLE / 2);
            if (scroll_offset < 0)
                scroll_offset = 0;
            redraw_needed = true;
        }
        return;
    }

    // Left-click to select menu item
    if (ev->type == ButtonPress && ev->button == Button1)
    {
        int input_box_y = PADDING;
        int input_box_height = FONT_SIZE + 12;
        int separator_y = input_box_y + input_box_height + 8;

        if (ev->y > separator_y + 10)
        {
            int item_y = separator_y + 10;
            int item_height = FONT_SIZE + 10;
            int click_item_index = (ev->y - item_y) / item_height;

            int visible_idx = 0;
            int match_idx = -1;
            for (int i = 0; i < item_count; i++)
            {
                if (items[i].is_match)
                {
                    if (visible_idx >= scroll_offset && visible_idx < scroll_offset + MAX_VISIBLE)
                    {
                        if (visible_idx - scroll_offset == click_item_index)
                        {
                            match_idx = i;
                            break;
                        }
                    }
                    visible_idx++;
                }
            }

            if (match_idx != -1)
            {
                selected_idx = match_idx;
                redraw_needed = true;
                execute_command(items[selected_idx].text);
            }
        }
    }
}

void handle_events()
{
    XEvent ev;
    while (XPending(dpy) > 0 && running)
    {
        XNextEvent(dpy, &ev);
        switch (ev.type)
        {
        case Expose:
            redraw_needed = true;
            break;

        case KeyPress:
            handle_key_press(&ev.xkey);
            break;

        case ButtonPress:
            handle_mouse_event(&ev.xbutton);
            break;
        }
    }
}

void render_offscreen()
{
    if (!redraw_needed)
        return;

    draw_rect(0, 0, WIDTH, HEIGHT, &color_bg, 1.0f);

    int input_box_x = PADDING;
    int input_box_y = PADDING;
    int input_box_width = WIDTH - 2 * PADDING;
    int input_box_height = FONT_SIZE + 12;
    draw_rect(input_box_x, input_box_y, input_box_width, input_box_height,
              &color_input_bg, COLOR_INPUT_BG_ALPHA);

    const char *prompt_text = "Filter >  ";
    int prompt_x = input_box_x + 8;
    int prompt_y = input_box_y + 6;
    draw_main_text(prompt_x, prompt_y, prompt_text, &color_text);
    int prompt_width = get_main_text_width(prompt_text);

    int input_x = prompt_x + prompt_width;
    draw_main_text(input_x, prompt_y, input_str, &color_text);

    int cursor_x = get_cursor_x_pos(input_x);
    draw_cursor(cursor_x, prompt_y);

    int separator_y = input_box_y + input_box_height + 8;
    draw_rect(PADDING, separator_y, WIDTH - 2 * PADDING, 1, &color_separator, 1.0f);

    int y_offset = separator_y + 10;
    int visible_count = 0;
    int match_count = 0;
    for (int i = 0; i < item_count; i++)
        if (items[i].is_match)
            match_count++;

    for (int i = 0; i < item_count && visible_count < MAX_VISIBLE; i++)
    {
        if (items[i].is_match)
        {
            if (i < scroll_offset)
                continue;

            if (i == selected_idx)
            {
                draw_rect(
                    PADDING, y_offset - 2,
                    WIDTH - 2 * PADDING, FONT_SIZE + 8,
                    &color_selection, 0.8f);
            }

            draw_main_text(PADDING + 10, y_offset, items[i].text, &color_text);

            y_offset += FONT_SIZE + 10;
            visible_count++;
        }
    }

    if (match_count > 0)
    {
        const char *hint_text = "PgUp/PgDn: Nav | Wheel: Scroll | Enter: Run | Esc: Quit";
        int hint_width = get_hint_text_width(hint_text);
        int hint_x = (WIDTH - hint_width) / 2;
        int hint_y = HEIGHT - PADDING - HINT_FONT_SIZE;
        draw_hint_text(hint_x, hint_y, hint_text, &color_hint);
    }

    redraw_needed = false;
}

void swap_buffers()
{
    XCopyArea(dpy, backbuffer, win, gc, 0, 0, WIDTH, HEIGHT, 0, 0);
    XFlush(dpy);
}

void cleanup_x11()
{
    if (child_pid > 0)
    {
        int status;
        waitpid(child_pid, &status, WNOHANG);
    }

    if (main_font)
        XftFontClose(dpy, main_font);
    if (hint_font)
        XftFontClose(dpy, hint_font);

    XftColorFree(dpy, vis, cmap, &color_bg);
    XftColorFree(dpy, vis, cmap, &color_input_bg);
    XftColorFree(dpy, vis, cmap, &color_separator);
    XftColorFree(dpy, vis, cmap, &color_text);
    XftColorFree(dpy, vis, cmap, &color_hint);
    XftColorFree(dpy, vis, cmap, &color_selection);

    if (backbuffer)
        XFreePixmap(dpy, backbuffer);
    if (gc)
        XFreeGC(dpy, gc);
    if (win)
        XDestroyWindow(dpy, win);

    if (dpy)
        XCloseDisplay(dpy);
}

long get_current_time_us()
{
    struct timespec ts;
    clock_gettime(CLOCK_MONOTONIC, &ts);
    return ts.tv_sec * 1000000 + ts.tv_nsec / 1000;
}

int main()
{
    if (!init_x11())
    {
        fprintf(stderr, "X11初始化失败\n");
        return 1;
    }

    read_items_from_stdin();
    filter_items();

    long last_frame_time = get_current_time_us();
    while (running)
    {
        long current_time = get_current_time_us();

        handle_events();

        check_child_process();

        if (redraw_needed)
        {
            render_offscreen();
            swap_buffers();
        }

        long elapsed = get_current_time_us() - current_time;
        if (elapsed < FRAME_TIME_US)
            usleep(FRAME_TIME_US - elapsed);

        last_frame_time = get_current_time_us();
    }

    cleanup_x11();
    return 0;
}
